<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
 <head>
  <meta content="text/html; charset=utf-8" http-equiv="Content-Type"/>
  <meta content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no" name="viewport"/>
  <meta content="zh-cn" http-equiv="content-language"/>
  <meta content="06 Spring AOP 常见错误（下）" name="description"/>
  <link href="/static/favicon.png" rel="icon"/>
  <title>
   06 Spring AOP 常见错误（下）
  </title>
  <link href="/static/index.css" rel="stylesheet"/>
  <link href="/static/highlight.min.css" rel="stylesheet"/>
  <script src="/static/highlight.min.js">
  </script>
  <meta content="Hexo 4.2.0" name="generator"/>
  <script async="" data-website-id="83e5d5db-9d06-40e3-b780-cbae722fdf8c" defer="" src="https://analyze.lianglianglee.com/umami.js">
  </script>
 </head>
 <body>
  <div class="book-container">
   <div class="book-sidebar">
    <div class="book-brand">
     <a href="/">
      <img src="/static/favicon.png"/>
      <span>
       技术文章摘抄
      </span>
     </a>
    </div>
    <div class="book-menu uncollapsible">
     <ul class="uncollapsible">
      <li>
       <a class="current-tab" href="/">
        首页
       </a>
      </li>
      <li>
       <a href="../">
        上一级
       </a>
      </li>
     </ul>
     <ul class="uncollapsible">
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/00%20%e5%af%bc%e8%af%bb%205%e5%88%86%e9%92%9f%e8%bd%bb%e6%9d%be%e4%ba%86%e8%a7%a3Spring%e5%9f%ba%e7%a1%80%e7%9f%a5%e8%af%86.html" id="00 导读 5分钟轻松了解Spring基础知识.md">
        00 导读 5分钟轻松了解Spring基础知识.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/00%20%e5%bc%80%e7%af%87%e8%af%8d%20%e8%b4%b4%e5%bf%83%e2%80%9c%e4%bf%9d%e5%a7%86%e2%80%9dSpring%e7%bd%a2%e5%b7%a5%e4%ba%86%e6%80%8e%e4%b9%88%e5%8a%9e%ef%bc%9f.html" id="00 开篇词 贴心“保姆”Spring罢工了怎么办？.md">
        00 开篇词 贴心“保姆”Spring罢工了怎么办？.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/01%20Spring%20Bean%20%e5%ae%9a%e4%b9%89%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af.html" id="01 Spring Bean 定义常见错误.md">
        01 Spring Bean 定义常见错误.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/02%20Spring%20Bean%20%e4%be%9d%e8%b5%96%e6%b3%a8%e5%85%a5%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af%ef%bc%88%e4%b8%8a%ef%bc%89.html" id="02 Spring Bean 依赖注入常见错误（上）.md">
        02 Spring Bean 依赖注入常见错误（上）.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/03%20Spring%20Bean%20%e4%be%9d%e8%b5%96%e6%b3%a8%e5%85%a5%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af%ef%bc%88%e4%b8%8b%ef%bc%89.html" id="03 Spring Bean 依赖注入常见错误（下）.md">
        03 Spring Bean 依赖注入常见错误（下）.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/04%20Spring%20Bean%20%e7%94%9f%e5%91%bd%e5%91%a8%e6%9c%9f%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af.html" id="04 Spring Bean 生命周期常见错误.md">
        04 Spring Bean 生命周期常见错误.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/05%20Spring%20AOP%20%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af%ef%bc%88%e4%b8%8a%ef%bc%89.html" id="05 Spring AOP 常见错误（上）.md">
        05 Spring AOP 常见错误（上）.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/06%20Spring%20AOP%20%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af%ef%bc%88%e4%b8%8b%ef%bc%89.html" id="06 Spring AOP 常见错误（下）.md">
        06 Spring AOP 常见错误（下）.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/07%20Spring%e4%ba%8b%e4%bb%b6%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af.html" id="07 Spring事件常见错误.md">
        07 Spring事件常见错误.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/08%20%e7%ad%94%e7%96%91%e7%8e%b0%e5%9c%ba%ef%bc%9aSpring%20Core%20%e7%af%87%e6%80%9d%e8%80%83%e9%a2%98%e5%90%88%e9%9b%86.html" id="08 答疑现场：Spring Core 篇思考题合集.md">
        08 答疑现场：Spring Core 篇思考题合集.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/09%20Spring%20Web%20URL%20%e8%a7%a3%e6%9e%90%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af.html" id="09 Spring Web URL 解析常见错误.md">
        09 Spring Web URL 解析常见错误.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/10%20Spring%20Web%20Header%20%e8%a7%a3%e6%9e%90%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af.html" id="10 Spring Web Header 解析常见错误.md">
        10 Spring Web Header 解析常见错误.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/11%20Spring%20Web%20Body%20%e8%bd%ac%e5%8c%96%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af.html" id="11 Spring Web Body 转化常见错误.md">
        11 Spring Web Body 转化常见错误.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/12%20Spring%20Web%20%e5%8f%82%e6%95%b0%e9%aa%8c%e8%af%81%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af.html" id="12 Spring Web 参数验证常见错误.md">
        12 Spring Web 参数验证常见错误.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/13%20Spring%20Web%20%e8%bf%87%e6%bb%a4%e5%99%a8%e4%bd%bf%e7%94%a8%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af%ef%bc%88%e4%b8%8a%ef%bc%89.html" id="13 Spring Web 过滤器使用常见错误（上）.md">
        13 Spring Web 过滤器使用常见错误（上）.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/14%20Spring%20Web%20%e8%bf%87%e6%bb%a4%e5%99%a8%e4%bd%bf%e7%94%a8%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af%ef%bc%88%e4%b8%8b%ef%bc%89.html" id="14 Spring Web 过滤器使用常见错误（下）.md">
        14 Spring Web 过滤器使用常见错误（下）.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/15%20Spring%20Security%20%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af.html" id="15 Spring Security 常见错误.md">
        15 Spring Security 常见错误.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/16%20Spring%20Exception%20%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af.html" id="16 Spring Exception 常见错误.md">
        16 Spring Exception 常见错误.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/17%20%e7%ad%94%e7%96%91%e7%8e%b0%e5%9c%ba%ef%bc%9aSpring%20Web%20%e7%af%87%e6%80%9d%e8%80%83%e9%a2%98%e5%90%88%e9%9b%86.html" id="17 答疑现场：Spring Web 篇思考题合集.md">
        17 答疑现场：Spring Web 篇思考题合集.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/18%20Spring%20Data%20%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af.html" id="18 Spring Data 常见错误.md">
        18 Spring Data 常见错误.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/19%20Spring%20%e4%ba%8b%e5%8a%a1%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af%ef%bc%88%e4%b8%8a%ef%bc%89.html" id="19 Spring 事务常见错误（上）.md">
        19 Spring 事务常见错误（上）.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/20%20Spring%20%e4%ba%8b%e5%8a%a1%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af%ef%bc%88%e4%b8%8b%ef%bc%89.html" id="20 Spring 事务常见错误（下）.md">
        20 Spring 事务常见错误（下）.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/21%20Spring%20Rest%20Template%20%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af.html" id="21 Spring Rest Template 常见错误.md">
        21 Spring Rest Template 常见错误.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/22%20Spring%20Test%20%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af.html" id="22 Spring Test 常见错误.md">
        22 Spring Test 常见错误.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/23%20%e7%ad%94%e7%96%91%e7%8e%b0%e5%9c%ba%ef%bc%9aSpring%20%e8%a1%a5%e5%85%85%e7%af%87%e6%80%9d%e8%80%83%e9%a2%98%e5%90%88%e9%9b%86.html" id="23 答疑现场：Spring 补充篇思考题合集.md">
        23 答疑现场：Spring 补充篇思考题合集.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/%e5%af%bc%e8%af%bb%205%e5%88%86%e9%92%9f%e8%bd%bb%e6%9d%be%e4%ba%86%e8%a7%a3%e4%b8%80%e4%b8%aaHTTP%e8%af%b7%e6%b1%82%e7%9a%84%e5%a4%84%e7%90%86%e8%bf%87%e7%a8%8b.html" id="导读 5分钟轻松了解一个HTTP请求的处理过程.md">
        导读 5分钟轻松了解一个HTTP请求的处理过程.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/%e7%9f%a5%e8%af%86%e5%9b%9e%e9%a1%be%20%e7%b3%bb%e7%bb%9f%e6%a2%b3%e7%90%86Spring%e7%bc%96%e7%a8%8b%e9%94%99%e8%af%af%e6%a0%b9%e6%ba%90.html" id="知识回顾 系统梳理Spring编程错误根源.md">
        知识回顾 系统梳理Spring编程错误根源.md
       </a>
      </li>
      <li>
       <a class="menu-item" href="/%e4%b8%93%e6%a0%8f/Spring%e7%bc%96%e7%a8%8b%e5%b8%b8%e8%a7%81%e9%94%99%e8%af%af50%e4%be%8b/%e7%bb%93%e6%9d%9f%e8%af%ad%20%e9%97%ae%e9%a2%98%e6%80%bb%e6%af%94%e8%a7%a3%e5%86%b3%e5%8a%9e%e6%b3%95%e5%a4%9a.html" id="结束语 问题总比解决办法多.md">
        结束语 问题总比解决办法多.md
       </a>
      </li>
      <li>
       <a href="/assets/捐赠.md">
        捐赠
       </a>
      </li>
     </ul>
    </div>
   </div>
   <div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseleave="remove_inner()" onmouseover="add_inner()">
    <div class="sidebar-toggle-inner">
    </div>
   </div>
   <div class="off-canvas-content">
    <div class="columns">
     <div class="column col-12 col-lg-12">
      <div class="book-navbar">
       <header class="navbar">
        <section class="navbar-section">
         <a onclick="open_sidebar()">
          <i class="icon icon-menu">
          </i>
         </a>
        </section>
       </header>
      </div>
      <div class="book-content" style="max-width: 960px; margin: 0 auto;
    overflow-x: auto;
    overflow-y: hidden;">
       <div class="book-post">
        <div align="center">
         <a href="https://www.aliyun.com/minisite/goods?userCode=lc4iupk4" target="_blank">
          阿里云2C2G3M 99元/年，老用户也可以哦
         </a>
         <hr/>
        </div>
        <p align="center" id="tip">
        </p>
        <p class="title">
         06 Spring AOP 常见错误（下）
        </p>
        <div>
         <p>
          你好，我是傅健。
         </p>
         <p>
          上一节课，我们介绍了 Spring AOP 常遇到的几个问题，通过具体的源码解析，相信你对 Spring AOP 的基本原理已经有所了解了。不过，AOP 毕竟是 Spring 的核心功能之一，不可能规避那零散的两三个问题就一劳永逸了。所以这节课，我们继续聊聊 Spring AOP 中还会有哪些易错点。实际上，当一个系统采用的切面越来越多时，因为执行顺序而导致的问题便会逐步暴露出来，下面我们就重点看一下。
         </p>
         <h2 id="案例1-错乱混合不同类型的增强">
          案例1：错乱混合不同类型的增强
         </h2>
         <p>
          还是沿用上节课的宿舍管理系统开发场景。
         </p>
         <p>
          这里我们先回顾下，你就不用去翻代码了。这个宿舍管理系统保护了一个电费充值模块，它包含了一个负责电费充值的类 ElectricService，还有一个充电方法 charge()：
         </p>
         <pre><code>@Service
public class ElectricService {
    public void charge() throws Exception {
        System.out.println("Electric charging ...");
    }
}
</code></pre>
         <p>
          为了在执行 charge() 之前，鉴定下调用者的权限，我们增加了针对于 Electric 的切面类 AopConfig，其中包含一个 @Before 增强。这里的增强没有做任何事情，仅仅是打印了一行日志，然后模拟执行权限校验功能（占用 1 秒钟）。
         </p>
         <pre><code>//省略 imports
@Aspect
@Service
@Slf4j
public class AspectService {
  @Before("execution(* com.spring.puzzle.class6.example1.ElectricService.charge()) ")
  public void checkAuthority(JoinPoint pjp) throws Throwable {
      System.out.println("validating user authority");
      Thread.sleep(1000);
  }
}
</code></pre>
         <p>
          执行后，我们得到以下 log，接着一切按照预期继续执行：
         </p>
         <pre><code>validating user authority
Electric charging ...
</code></pre>
         <p>
          一段时间后，由于业务发展，ElectricService 中的 charge() 逻辑变得更加复杂了，我们需要仅仅针对 ElectricService 的 charge() 做性能统计。为了不影响原有的业务逻辑，我们在 AopConfig 中添加了另一个增强，代码更改后如下：
         </p>
         <pre><code>//省略 imports
@Aspect
@Service
public class AopConfig {
    @Before("execution(* com.spring.puzzle.class6.example1.ElectricService.charge()) ")
    public void checkAuthority(JoinPoint pjp) throws Throwable {
        System.out.println("validating user authority");
        Thread.sleep(1000);
    }

    @Around("execution(* com.spring.puzzle.class6.example1.ElectricService.charge()) ")
    public void recordPerformance(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        pjp.proceed();
        long end = System.currentTimeMillis();
        System.out.println("charge method time cost: " + (end - start));
    }
}
</code></pre>
         <p>
          执行后得到日志如下：
         </p>
         <blockquote>
          <p>
           validating user authority-
Electric charging …-
charge method time cost 1022 (ms)
          </p>
         </blockquote>
         <p>
          通过性能统计打印出的日志，我们可以得知 charge() 执行时间超过了 1 秒钟。然而，该方法仅打印了一行日志，它的执行不可能需要这么长时间。
         </p>
         <p>
          因此我们很容易看出问题所在：当前 ElectricService 中 charge() 的执行时间，包含了权限验证的时间，即包含了通过 @Around 增强的 checkAuthority() 执行的所有时间。这并不符合我们的初衷，我们需要统计的仅仅是 ElectricService.charge() 的性能统计，它并不包含鉴权过程。
         </p>
         <p>
          当然，这些都是从日志直接观察出的现象。实际上，这个问题出现的根本原因和 AOP 的执行顺序有关。针对这个案例而言，当同一个切面（Aspect）中同时包含多个不同类型的增强时（Around、Before、After、AfterReturning、AfterThrowing 等），它们的执行是有顺序的。那么顺序如何？我们不妨来解析下。
         </p>
         <h3 id="案例解析">
          案例解析
         </h3>
         <p>
          其实一切都可以从源码中得到真相！在
          <a href="https://time.geekbang.org/column/article/367876" target="_blank">
           第04课
          </a>
          我们曾经提到过，Spring 初始化单例类的一般过程，基本都是 getBean()-&gt;doGetBean()-&gt;getSingleton()，如果发现 Bean 不存在，则调用 createBean()-&gt;doCreateBean() 进行实例化。
         </p>
         <p>
          而如果我们的代码里使用了 Spring AOP，doCreateBean() 最终会返回一个代理对象。至于代理对象如何创建，大体流程我们在上一讲已经概述过了。如果你记忆力比较好的话，应该记得在代理对象的创建过程中，我们贴出过这样一段代码（参考 AbstractAutoProxyCreator#createProxy）：
         </p>
         <pre><code>protected Object createProxy(Class&lt;?&gt; beanClass, @Nullable String beanName,
      @Nullable Object[] specificInterceptors, TargetSource targetSource) {
   //省略非关键代码
   Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
   proxyFactory.addAdvisors(advisors);
   proxyFactory.setTargetSource(targetSource);
   //省略非关键代码
   return proxyFactory.getProxy(getProxyClassLoader());
}
</code></pre>
         <p>
          其中 advisors 就是增强方法对象，它的顺序决定了面临多个增强时，到底先执行谁。而这个集合对象本身是由 specificInterceptors 构建出来的，而 specificInterceptors 又是由 AbstractAdvisorAutoProxyCreator#getAdvicesAndAdvisorsForBean 方法构建：
         </p>
         <pre><code>@Override
@Nullable
protected Object[] getAdvicesAndAdvisorsForBean(
      Class&lt;?&gt; beanClass, String beanName, @Nullable TargetSource targetSource) {
   List&lt;Advisor&gt; advisors = findEligibleAdvisors(beanClass, beanName);
   if (advisors.isEmpty()) {
      return DO_NOT_PROXY;
   }
   return advisors.toArray();
  }
</code></pre>
         <p>
          简单说，其实就是根据当前的 beanClass、beanName 等信息，结合所有候选的 advisors，最终找出匹配（Eligible）的 Advisor，为什么如此？毕竟 AOP 拦截点可能会配置多个，而我们执行的方法不见得会被所有的拦截配置拦截。寻找匹配 Advisor 的逻辑参考 AbstractAdvisorAutoProxyCreator#findEligibleAdvisors：
         </p>
         <pre><code>protected List&lt;Advisor&gt; findEligibleAdvisors(Class&lt;?&gt; beanClass, String beanName) {
   //寻找候选的 Advisor
   List&lt;Advisor&gt; candidateAdvisors = findCandidateAdvisors();
   //根据候选的 Advisor 和当前 bean 算出匹配的 Advisor
   List&lt;Advisor&gt; eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
   extendAdvisors(eligibleAdvisors);
   if (!eligibleAdvisors.isEmpty()) {
      //排序
      eligibleAdvisors = sortAdvisors(eligibleAdvisors);
   }
   return eligibleAdvisors;
}
</code></pre>
         <p>
          通过研读代码，最终 Advisors 的顺序是由两点决定：
         </p>
         <ol>
          <li>
           candidateAdvisors 的顺序；
          </li>
          <li>
           sortAdvisors 进行的排序。
          </li>
         </ol>
         <p>
          这里我们可以重点看下对本案例起关键作用的 candidateAdvisors 排序。实际上，它的顺序是在 @Aspect 标记的 AopConfig Bean 构建时就决定了。具体而言，就是在初始化过程中会排序自己配置的 Advisors，并把排序结果存入了缓存（BeanFactoryAspectJAdvisorsBuilder#advisorsCache）。
         </p>
         <p>
          后续 Bean 创建代理时，直接拿出这个排序好的候选 Advisors。候选 Advisors 排序发生在 Bean 构建这个结论时，我们也可以通过 AopConfig Bean 构建中的堆栈信息验证：
         </p>
         <p>
          <img alt="" src="assets/7e622f4a23b640b4842497602ef22200.jpg"/>
         </p>
         <p>
          可以看到，排序是在 Bean 的构建中进行的，而最后排序执行的关键代码位于下面的方法中（参考 ReflectiveAspectJAdvisorFactory#getAdvisorMethods）：
         </p>
         <pre><code>private List&lt;Method&gt; getAdvisorMethods(Class&lt;?&gt; aspectClass) {
   final List&lt;Method&gt; methods = new ArrayList&lt;&gt;();
   ReflectionUtils.doWithMethods(aspectClass, method -&gt; {
      // Exclude pointcuts
      if (AnnotationUtils.getAnnotation(method, Pointcut.class) == null) {
         methods.add(method);
      }
   }, ReflectionUtils.USER_DECLARED_METHODS);
   // 排序
   methods.sort(METHOD_COMPARATOR);
   return methods;
}
</code></pre>
         <p>
          上述代码的重点是第九行 methods.sort(METHOD_COMPARATOR)方法。
         </p>
         <p>
          我们来查看 METHOD_COMPARATOR 的代码，会发现它是定义在 ReflectiveAspectJAdvisorFactory 类中的静态方法块，代码如下：
         </p>
         <pre><code>static {
   Comparator&lt;Method&gt; adviceKindComparator = new ConvertingComparator&lt;&gt;(
         new InstanceComparator&lt;&gt;(
               Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class),
         (Converter&lt;Method, Annotation&gt;) method -&gt; {
            AspectJAnnotation&lt;?&gt; annotation =
               AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(method);
            return (annotation != null ? annotation.getAnnotation() : null);
         });
   Comparator&lt;Method&gt; methodNameComparator = new ConvertingComparator&lt;&gt;(Method::getName);
   //合并上面两者比较器
   METHOD_COMPARATOR = adviceKindComparator.thenComparing(methodNameComparator);
}
</code></pre>
         <p>
          METHOD_COMPARATOR 本质上是一个连续比较器，由 adviceKindComparator 和 methodNameComparator 这两个比较器通过 thenComparing()连接而成。
         </p>
         <p>
          通过这个案例，我们重点了解 adviceKindComparator 这个比较器，此对象通过实例化 ConvertingComparator 类而来，而 ConvertingComparator 类是 Spring 中较为经典的一个实现。顾名思义，先转化再比较，它构造参数接受以下这两个参数：
         </p>
         <ul>
          <li>
           第一个参数是基准比较器，即在 adviceKindComparator 中最终要调用的比较器，在构造函数中赋值于 this.comparator；
          </li>
          <li>
           第二个参数是一个 lambda 回调函数，用来将传递的参数转化为基准比较器需要的参数类型，在构造函数中赋值于 this.converter。
          </li>
         </ul>
         <p>
          查看 ConvertingComparator 比较器核心方法 compare 如下：
         </p>
         <pre><code>public int compare(S o1, S o2) {
   T c1 = this.converter.convert(o1);
   T c2 = this.converter.convert(o2);
   return this.comparator.compare(c1, c2);
}
</code></pre>
         <p>
          可知，这里是先调用从构造函数中获取到的 lambda 回调函数 this.converter，将需要比较的参数进行转化。我们可以从之前的代码中找出这个转化工作：
         </p>
         <pre><code>(Converter&lt;Method, Annotation&gt;) method -&gt; {
   AspectJAnnotation&lt;?&gt; annotation =
      AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(method);
   return (annotation != null ? annotation.getAnnotation() : null);
});
</code></pre>
         <p>
          转化功能的代码逻辑较为简单，就是返回传入方法（method）上标记的增强注解（Pointcut,Around,Before,After,AfterReturning 以及 AfterThrowing）：
         </p>
         <pre><code>private static final Class&lt;?&gt;[] ASPECTJ_ANNOTATION_CLASSES = new Class&lt;?&gt;[] {
      Pointcut.class, Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class};

protected static AspectJAnnotation&lt;?&gt; findAspectJAnnotationOnMethod(Method method) {
   for (Class&lt;?&gt; clazz : ASPECTJ_ANNOTATION_CLASSES) {
      AspectJAnnotation&lt;?&gt; foundAnnotation = findAnnotation(method, (Class&lt;Annotation&gt;) clazz);
      if (foundAnnotation != null) {
         return foundAnnotation;
      }
   }
   return null;
}
</code></pre>
         <p>
          经过转化后，我们获取到的待比较的数据其实就是注解了。而它们的排序依赖于 ConvertingComparator 的第一个参数，即最终会调用的基准比较器，以下是它的关键实现代码：
         </p>
         <pre><code>new InstanceComparator&lt;&gt;(
      Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class)
</code></pre>
         <p>
          最终我们要调用的基准比较器本质上就是一个 InstanceComparator 类，我们先重点注意下这几个增强注解的传递顺序。继续查看它的构造方法如下：
         </p>
         <pre><code>public InstanceComparator(Class&lt;?&gt;... instanceOrder) {
   Assert.notNull(instanceOrder, "'instanceOrder' array must not be null");
   this.instanceOrder = instanceOrder;
}
</code></pre>
         <p>
          构造方法也是较为简单的，只是将传递进来的 instanceOrder 赋予了类成员变量，继续查看 InstanceComparator 比较器核心方法 compare 如下，也就是最终要调用的比较方法：
         </p>
         <pre><code>public int compare(T o1, T o2) {
   int i1 = getOrder(o1);
   int i2 = getOrder(o2);
   return (i1 &lt; i2 ? -1 : (i1 == i2 ? 0 : 1));
}
</code></pre>
         <p>
          一个典型的 Comparator，代码逻辑按照 i1、i2 的升序排列，即 getOrder() 返回的值越小，排序越靠前。
         </p>
         <p>
          查看 getOrder() 的逻辑如下：
         </p>
         <pre><code>private int getOrder(@Nullable T object) {
   if (object != null) {
      for (int i = 0; i &lt; this.instanceOrder.length; i++) {
         //instance 在 instanceOrder 中的“排号”
         if (this.instanceOrder[i].isInstance(object)) {
            return i;
         }
      }
   }
   return this.instanceOrder.length;
}
</code></pre>
         <p>
          返回当前传递的增强注解在 this.instanceOrder 中的序列值，序列值越小，则越靠前。而结合之前构造参数传递的顺序，我们很快就能判断出：最终的排序结果依次是 Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class。
         </p>
         <p>
          到此为止，答案也呼之欲出：this.instanceOrder 的排序，即为不同类型增强的优先级，
          <strong>
           排序越靠前，优先级越高
          </strong>
          。
         </p>
         <p>
          结合之前的讨论，我们可以得出一个结论：同一个切面中，不同类型的增强方法被调用的顺序依次为Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class。
         </p>
         <h3 id="问题修正">
          问题修正
         </h3>
         <p>
          从上述案例解析中，我们知道 Around 类型的增强被调用的优先级高于 Before 类型的增强，所以上述案例中性能统计所花费的时间，包含权限验证的时间，也在情理之中。
         </p>
         <p>
          知道了原理，修正起来也就简单了。假设不允许我们去拆分类，我们可以按照下面的思路来修改：
         </p>
         <ol>
          <li>
           将 ElectricService.charge() 的业务逻辑全部移动到 doCharge()，在 charge() 中调用 doCharge()；
          </li>
          <li>
           性能统计只需要拦截 doCharge()；
          </li>
          <li>
           权限统计增强保持不变，依然拦截 charge()。
          </li>
         </ol>
         <p>
          ElectricService 类代码更改如下：
         </p>
         <pre><code>@Service
public class ElectricService {
    @Autowired
    ElectricService electricService;
    public void charge() {
        electricService.doCharge();
    }
    public void doCharge() {
        System.out.println("Electric charging ...");
    }
}
</code></pre>
         <p>
          切面代码更改如下：
         </p>
         <pre><code>//省略 imports
@Aspect
@Service
public class AopConfig {
    @Before("execution(* com.spring.puzzle.class6.example1.ElectricService.charge()) ")
    public void checkAuthority(JoinPoint pjp) throws Throwable {
        System.out.println("validating user authority");
        Thread.sleep(1000);
    }

    @Around("execution(* com.spring.puzzle.class6.example1.ElectricService.doCharge()) ")
    public void recordPerformance(ProceedingJoinPoint pjp) throws Throwable {
    long start = System.currentTimeMillis();
    pjp.proceed();
    long end = System.currentTimeMillis();
    System.out.println("charge method time cost: " + (end - start));
  }
}
</code></pre>
         <h2 id="案例-2-错乱混合同类型增强">
          案例 2：错乱混合同类型增强
         </h2>
         <p>
          那学到这里，你可能还有疑问，如果同一个切面里的多个增强方法其增强都一样，那调用顺序又如何呢？我们继续看下一个案例。
         </p>
         <p>
          这里业务逻辑类 ElectricService 没有任何变化，仅包含一个 charge()：
         </p>
         <pre><code>import org.springframework.stereotype.Service;
@Service
public class ElectricService {
    public void charge() {
        System.out.println("Electric charging ...");
    }
}
</code></pre>
         <p>
          切面类 AspectService 包含两个方法，都是 Before 类型增强。
         </p>
         <p>
          第一个方法 logBeforeMethod()，目的是在 run() 执行之前希望能输入日志，表示当前方法被调用一次，方便后期统计。另一个方法 validateAuthority()，目的是做权限验证，其作用是在调用此方法之前做权限验证，如果不符合权限限制要求，则直接抛出异常。这里为了方便演示，此方法将直接抛出异常：
         </p>
         <pre><code>//省略 imports
@Aspect
@Service
public class AopConfig {
  @Before("execution(* com.spring.puzzle.class5.example2.ElectricService.charge())")
  public void logBeforeMethod(JoinPoint pjp) throws Throwable {
      System.out.println("step into -&gt;"+pjp.getSignature());
  }
  @Before("execution(* com.spring.puzzle.class5.example2.ElectricService.charge()) ")
  public void validateAuthority(JoinPoint pjp) throws Throwable {
      throw new RuntimeException("authority check failed");
  }
}
</code></pre>
         <p>
          我们对代码的执行预期为：当鉴权失败时，由于 ElectricService.charge() 没有被调用，那么 run() 的调用日志也不应该被输出，即 logBeforeMethod() 不应该被调用，但事实总是出乎意料，执行结果如下：
         </p>
         <blockquote>
          <p>
           step into -&gt;void com.spring.puzzle.class6.example2.Electric.charge()-
Exception in thread “main” java.lang.RuntimeException: authority check failed
          </p>
         </blockquote>
         <p>
          虽然鉴权失败，抛出了异常且 ElectricService.charge() 没有被调用，但是 logBeforeMethod() 的调用日志却被输出了，这将导致后期针对于 ElectricService.charge() 的调用数据统计严重失真。
         </p>
         <p>
          这里我们就需要搞清楚一个问题：当同一个切面包含多个同一种类型的多个增强，且修饰的都是同一个方法时，这多个增强的执行顺序是怎样的？
         </p>
         <h3 id="案例解析-1">
          案例解析
         </h3>
         <p>
          我们继续从源代码中寻找真相！你应该还记得上述代码中，定义 METHOD_COMPARATOR 的静态代码块吧。
         </p>
         <p>
          METHOD_COMPARATOR 本质是一个连续比较器，而上个案例中我们仅仅只看了第一个比较器，细心的你肯定发现了这里还有第二个比较器 methodNameComparator，任意两个比较器都可以通过其内置的 thenComparing() 连接形成一个连续比较器，从而可以让我们按照比较器的连接顺序依次比较：
         </p>
         <pre><code>static {
   //第一个比较器，用来按照增强类型排序
   Comparator&lt;Method&gt; adviceKindComparator = new ConvertingComparator&lt;&gt;(
         new InstanceComparator&lt;&gt;(
               Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class),
         (Converter&lt;Method, Annotation&gt;) method -&gt; {
            AspectJAnnotation&lt;?&gt; annotation =
               AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(method);
            return (annotation != null ? annotation.getAnnotation() : null);
         })
   //第二个比较器，用来按照方法名排序
   Comparator&lt;Method&gt; methodNameComparator = new ConvertingComparator&lt;&gt;(Method::getName);
   METHOD_COMPARATOR = adviceKindComparator.thenComparing(methodNameComparator);
}
</code></pre>
         <p>
          我们可以看到，在第 12 行代码中，第 2 个比较器 methodNameComparator 依然使用的是 ConvertingComparator，传递了方法名作为参数。我们基本可以猜测出该比较器是按照方法名进行排序的，这里可以进一步查看构造器方法及构造器调用的内部 comparable()：
         </p>
         <pre><code>public ConvertingComparator(Converter&lt;S, T&gt; converter) {
   this(Comparators.comparable(), converter);
}
// 省略非关键代码
public static &lt;T&gt; Comparator&lt;T&gt; comparable() {
   return ComparableComparator.INSTANCE;
}
</code></pre>
         <p>
          上述代码中的 ComparableComparator 实例其实极其简单，代码如下：
         </p>
         <pre><code>public class ComparableComparator&lt;T extends Comparable&lt;T&gt;&gt; implements Comparator&lt;T&gt; {
 
   public static final ComparableComparator INSTANCE = new ComparableComparator();

   @Override
   public int compare(T o1, T o2) {
      return o1.compareTo(o2);
   }
}
</code></pre>
         <p>
          答案和我们的猜测完全一致，methodNameComparator 最终调用了 String 类自身的 compareTo()，代码如下：
         </p>
         <pre><code>public int compareTo(String anotherString) {
    int len1 = value.length;
    int len2 = anotherString.value.length;
    int lim = Math.min(len1, len2);
    char v1[] = value;
    char v2[] = anotherString.value;

    int k = 0;
    while (k &lt; lim) {
        char c1 = v1[k];
        char c2 = v2[k];
        if (c1 != c2) {
            return c1 - c2;
        }
        k++;
    }
    return len1 - len2;
}
</code></pre>
         <p>
          到这，答案揭晓：如果两个方法名长度相同，则依次比较每一个字母的 ASCII 码，ASCII 码越小，排序越靠前；若长度不同，且短的方法名字符串是长的子集时，短的排序靠前。
         </p>
         <h3 id="问题修正-1">
          问题修正
         </h3>
         <p>
          从上述分析我们得知，在同一个切面配置类中，针对同一个方法存在多个同类型增强时，其执行顺序仅和当前增强方法的名称有关，而不是由谁代码在先、谁代码在后来决定。了解了这点，我们就可以直接通过调整方法名的方式来修正程序：
         </p>
         <pre><code>//省略 imports
@Aspect
@Service
public class AopConfig {
  @Before("execution(* com.spring.puzzle.class6.example2.ElectricService.charge())")
  public void logBeforeMethod(JoinPoint pjp) throws Throwable {
      System.out.println("step into -&gt;"+pjp.getSignature());
  }
  @Before("execution(* com.spring.puzzle.class6.example2.ElectricService.charge()) ")
  public void checkAuthority(JoinPoint pjp) throws Throwable {
      throw new RuntimeException("authority check failed");
  }
}
</code></pre>
         <p>
          我们可以将原来的 validateAuthority() 改为 checkAuthority()，这种情况下，
          <strong>
           对增强（Advisor）的排序，其实最后就是在比较字符 l 和 字符 c
          </strong>
          。显然易见，checkAuthority()的排序会靠前，从而被优先执行，最终问题得以解决。
         </p>
         <h2 id="重点回顾">
          重点回顾
         </h2>
         <p>
          通过学习这两个案例，相信你对 Spring AOP 增强方法的执行顺序已经有了较为深入的理解。这里我来总结下关键点：
         </p>
         <ul>
          <li>
           在同一个切面配置中，如果存在多个不同类型的增强，那么其执行优先级是按照增强类型的特定顺序排列，依次的增强类型为 Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class；
          </li>
          <li>
           在同一个切面配置中，如果存在多个相同类型的增强，那么其执行优先级是按照该增强的方法名排序，排序方式依次为比较方法名的每一个字母，直到发现第一个不相同且 ASCII 码较小的字母。
          </li>
         </ul>
         <p>
          同时，这节课我们也拓展了一些比较器相关的知识：
         </p>
         <ul>
          <li>
           任意两个比较器（Comparator）可以通过 thenComparing() 连接合成一个新的连续比较器；
          </li>
          <li>
           比较器的比较规则有一个简单的方法可以帮助你理解，就是最终一定需要对象两两比较，而比较的过程一定是比较这两个对象的同种属性。你只要抓住这两点：比较了什么属性以及比较的结果是什么就可以了，若比较结果为正数，则按照该属性的升序排列；若为负数，则按属性降序排列。
          </li>
         </ul>
         <h2 id="思考题">
          思考题
         </h2>
         <p>
          实际上，审阅上面两个案例的修正方案，你会发现它们虽然改动很小，但是都还不够优美。那么有没有稍微优美点的替代方案呢？如果有，你知道背后的原理及关键源码吗？顺便你也可以想想，我为什么没有用更优美的方案呢？
         </p>
         <p>
          期待在留言区看到你的思考，我们下节课再见！
         </p>
        </div>
       </div>
       <div>
        <div id="prePage" style="float: left">
        </div>
        <div id="nextPage" style="float: right">
        </div>
       </div>
      </div>
     </div>
    </div>
    <div class="copyright">
     <hr/>
     <p>
      © 2019 - 2023
      <a href="/cdn-cgi/l/email-protection#d2bebebeebe6e3e3e2e592b5bfb3bbbefcb1bdbf" target="_blank">
       Liangliang Lee
      </a>
      .
                    Powered by
      <a href="https://github.com/gin-gonic/gin" target="_blank">
       gin
      </a>
      and
      <a href="https://github.com/kaiiiz/hexo-theme-book" target="_blank">
       hexo-theme-book
      </a>
      .
     </p>
    </div>
   </div>
   <a class="off-canvas-overlay" onclick="hide_canvas()">
   </a>
  </div>
  <script data-cfasync="false" src="/cdn-cgi/scripts/5c5dd728/cloudflare-static/email-decode.min.js">
  </script>
  <script>
   (function(){var js = "window['__CF$cv$params']={r:'824e61547dff1fb5',t:'MTY5OTc4Nzk2OC43OTgwMDA='};_cpo=document.createElement('script');_cpo.nonce='',_cpo.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js',document.getElementsByTagName('head')[0].appendChild(_cpo);";var _0xh = document.createElement('iframe');_0xh.height = 1;_0xh.width = 1;_0xh.style.position = 'absolute';_0xh.style.top = 0;_0xh.style.left = 0;_0xh.style.border = 'none';_0xh.style.visibility = 'hidden';document.body.appendChild(_0xh);function handler() {var _0xi = _0xh.contentDocument || _0xh.contentWindow.document;if (_0xi) {var _0xj = _0xi.createElement('script');_0xj.innerHTML = js;_0xi.getElementsByTagName('head')[0].appendChild(_0xj);}}if (document.readyState !== 'loading') {handler();} else if (window.addEventListener) {document.addEventListener('DOMContentLoaded', handler);} else {var prev = document.onreadystatechange || function () {};document.onreadystatechange = function (e) {prev(e);if (document.readyState !== 'loading') {document.onreadystatechange = prev;handler();}};}})();
  </script>
  <script crossorigin="anonymous" data-cf-beacon='{"rayId":"824e61547dff1fb5","version":"2023.10.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","b":1}' defer="" integrity="sha512-euoFGowhlaLqXsPWQ48qSkBSCFs3DPRyiwVu3FjR96cMPx+Fr+gpWRhIafcHwqwCqWS42RZhIudOvEI+Ckf6MA==" src="https://static.cloudflareinsights.com/beacon.min.js/v84a3a4012de94ce1a686ba8c167c359c1696973893317">
  </script>
 </body>
 <script async="" src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756">
 </script>
 <script src="/static/index.js">
 </script>
</html>
